在學習了如何使用 List
來顯示家用品清單後,今天我們要更進一步,實作讓使用者能夠新增和刪除家用品項目的功能。
我們首先來實作新增項目的功能。這裡我們會用到一個簡單的 TextField
讓使用者輸入項目名稱,並用一個 Button
來觸發新增操作。我們的目標 UI 如圖所示:
可以看到,上方有兩個 TextField
和一個 Button
水平排列,下方則是我們昨天已經製作好的列表。接下來,我們先來修改一下佈局。還記得我們之前介紹過的 VStack
和 HStack
嗎?如果忘記了,記得回去複習一下 Day4 的文章唷!
因為列表和輸入框是垂直排列的,所以我們需要先加入 VStack
,將 List
包起來。如果不想手動輸入程式碼,也可以在 List
的位置按下滑鼠右鍵,然後選擇「Embed in VStack」,這樣就能快速完成這一步囉!
VStack {
List(viewModel.items) { item in
HStack {
Text(item.name)
.font(.headline)
Spacer()
Text("數量: \(item.quantity)")
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.vertical, 8)
}
}
你會發現,這時候 UI 預覽並沒有發生什麼變化,因為我們還沒有加上其他元件。還記得示意圖中的 TextField
和 Button
是怎麼排列的嗎?沒錯,它們是水平排列的,所以我們需要使用 HStack
。接下來,我們來加入 TextField
、Button
和 HStack
吧!
TextField
是 SwiftUI 中用來接受使用者文字輸入的元件,類似 UIKit 中的 UITextField
。它允許使用者在 App 中輸入資料,並將這些資料綁定到某個變數,以便後續處理。TextField
是實現表單、搜尋框等功能的關鍵元件。
在建立 TextField
時,它的初始化方法接受型別 Binding<String>
的參數,所以我們必須傳入型別為 String 的變數並將其綁定。這樣當使用者輸入文字時,TextField
綁定的資料就會自動更新。我們來增加它綁定的值:
@State private var newItemName: String = ""
@State private var newItemQuantity: String = ""
參考資料:
接著,我們可以繼續完成排版:
VStack {
HStack {
TextField("輸入家用品名稱", text: $newItemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("數量", text: $newItemQuantity)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
print("Button Click")
}) {
Text("新增")
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
List(viewModel.items) { item in
HStack {
Text(item.name)
.font(.headline)
Spacer()
Text("數量: \(item.quantity)")
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.vertical, 8)
}
}
這時候,我們的排版和示意圖已經相當接近了,但還少了上方的標題列,這時候就要請出我們的好幫手— NavigationView
啦!
NavigationView
是 SwiftUI 中的一個容器元件,主要用來建立層次結構式的導航界面。與 UIKit 中的 UINavigationController
類似,可以讓使用者在多個畫面之間切換,並提供標題和返回按鈕等導覽列功能。
為了利用 NavigationView 管理多層頁面的切換,我們必須用 NavigationView
包住它管理的第一頁畫面,之後就可以切換到第二頁、第三頁。這時候可以呼叫 navigationTitle
這個方法來設定標題欄上的文字。因此,我們從 VStack
呼叫 navigationTitle
,將標題設為「家用品清單」。
NavigationView {
VStack {
HStack {
TextField("輸入家用品名稱", text: $newItemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("數量", text: $newItemQuantity)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
print("Button Click")
}) {
Text("新增")
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
List(viewModel.items) { item in
HStack {
Text(item.name)
Spacer()
Text("數量: \(item.quantity)")
}
}
.navigationTitle("家用品清單")
}
}
這樣一來,我們的 UI 就已經和示意圖完全一致了!
但這還沒完成,目前點擊 Button
只會在主控台印出「Button Click」。我們需要在 Button
的 action
裡面加入程式碼,讓它呼叫 ViewModel 中的 addItem
方法,這樣才能實現點擊按鈕新增項目的功能。讓我們來改寫一下 Button
的程式碼吧!
Button(action: {
if let quantity = Int(newItemQuantity), !newItemName.isEmpty {
viewModel.addItem(name: newItemName, quantity: quantity)
newItemName = ""
newItemQuantity = ""
}
}) {
Text("新增")
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
這樣就大功告成啦!來看看我們完成的結果吧!
如果剛剛的步驟拆解不夠清楚,這裡提供完整的程式碼供參考唷!
struct ContentView: View {
@StateObject var viewModel = ItemViewModel()
@State private var newItemName: String = ""
@State private var newItemQuantity: String = ""
var body: some View {
NavigationView {
VStack {
HStack {
TextField("輸入家用品名稱", text: $newItemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("數量", text: $newItemQuantity)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
if let quantity = Int(newItemQuantity), !newItemName.isEmpty {
viewModel.addItem(name: newItemName, quantity: quantity)
newItemName = ""
newItemQuantity = ""
}
}) {
Text("新增")
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
List(viewModel.items) { item in
HStack {
Text(item.name)
Spacer()
Text("數量: \(item.quantity)")
}
}
.navigationTitle("家用品清單")
}
}
}
}
參考資料:
完成新增功能後,使用者就可以新增項目到列表中。但是,有時候難免會因為手誤而新增了錯誤的項目,這時候就需要有刪除功能了。所以,接下來我們就來實作刪除功能吧!
在 List
中,我們還可以很容易地實作刪除功能。在這裡我們使用 onDelete
Modifier 來實現。
在 SwiftUI 中,onDelete()
是一個非常實用的 Modifier,用於在 List
中實現刪除行為。它允許使用者通過滑動操作來刪除列表中的特定項目。當配合 ForEach
使用時,onDelete()
可以自動處理刪除操作,並更新畫面。
使用 onDelete()
的基本語法如下:
.onDelete(perform: deleteItems)
其中,deleteItems 是一個函數,負責處理實際的刪除邏輯。這個修飾符需要與 ForEach
結合使用,讓每個列表項目都有唯一的識別碼,從而正確執行刪除操作。
在我們的原始程式碼中,List
直接接收了 viewModel.items,並為每個項目生成了對應的畫面。然而,為了在列表中實作刪除功能,我們需要對這段程式碼進行一些改寫,將其與 onDelete()
Modifier 結合使用。
原始程式碼如下:
List(viewModel.items) { item in
HStack {
Text(item.name)
.font(.headline)
Spacer()
Text("數量: \(item.quantity)")
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.vertical, 8)
}
這段程式碼雖然可以正常顯示列表,但無法直接支持刪除功能。為了讓使用者能夠刪除不需要的項目,我們需要將 List
的內容改寫為 ForEach
結構,並增加 onDelete()
Modifier,如下所示:
List {
ForEach(viewModel.items) { item in
HStack {
Text(item.name)
.font(.headline)
Spacer()
Text("數量: \(item.quantity)")
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.vertical, 8)
}
.onDelete(perform: deleteItems)
}
接下來,我們需要實作 deleteItems 函數,以處理實際的刪除操作。以下是一個簡單的實作範例:
func deleteItems(at offsets: IndexSet) {
viewModel.items.remove(atOffsets: offsets)
}
這個函數接收一個 IndexSet
,表示要刪除的項目在列表中的索引。通過調用 remove(atOffsets:)
方法,我們可以從 viewModel.items 中刪除相應的項目,並自動更新畫面。
整合後的完整程式碼如下:
struct ContentView: View {
@StateObject var viewModel = ItemViewModel()
@State private var newItemName: String = ""
@State private var newItemQuantity: String = ""
var body: some View {
NavigationView {
VStack {
HStack {
TextField("輸入家用品名稱", text: $newItemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("數量", text: $newItemQuantity)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
if let quantity = Int(newItemQuantity), !newItemName.isEmpty {
viewModel.addItem(name: newItemName, quantity: quantity)
newItemName = ""
newItemQuantity = ""
}
}) {
Text("新增")
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
List {
ForEach(viewModel.items) { item in
HStack {
Text(item.name)
Spacer()
Text("數量: \(item.quantity)")
}
}
.onDelete(perform: deleteItems)
}
.navigationTitle("家用品清單")
}
}
}
private func deleteItems(at offsets: IndexSet) {
viewModel.items.remove(atOffsets: offsets)
}
}
這樣一來,使用者就能夠在列表中滑動刪除不需要的項目,並且畫面會自動更新。
今天我們成功讓家用品清單活了起來,使用者現在可以新增和刪除項目,不再是死氣沈沈的靜態列表。透過這些實作,我們也進一步加深了對 SwiftUI 中資料管理和 UI 更新的理解。
明天,我們將開始學習如何將這些資料儲存到本地的 Core Data 中,讓我們的 App 能夠持久保存這些家用品項目。明天見!